﻿#region Copyright(c) 2006 ZO, All right reserved.
// -----------------------------------------------------------------------------
// Copyright(c) 2006 ZO, All right reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   1.  Redistribution of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//   2.  Redistribution in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//   3.  The name of the author may not be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// -----------------------------------------------------------------------------
#endregion


#region Using directives

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using ZO.SmartCore.Plugins.Properties;
using ZO.SmartCore.Reflection;
using ZO.SmartCore.Threading;
using ArgumentException= ZO.SmartCore.Core.ArgumentException;
using ArgumentNullException= ZO.SmartCore.Core.ArgumentNullException;
using ZO.SmartCore.My4CSharp;

#endregion

namespace ZO.SmartCore.Plugins
{
    /// <summary>
    /// Manages the dynamic loading of assemblies and the instantiation of plugins within those assemblies. 
    /// </summary>
    public sealed class PluginManager
    {

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="PluginManager"/> class.
        /// </summary>
		private PluginManager()
		{
			pluginAssemblies = new Dictionary<string, PluginAssembly>();
			plugins = new Dictionary<int, Plugin>();

		}

        #endregion

        #region Destructor

        #endregion

        #region Fields
    
		private static bool initialize = false;

        /// <summary>
        /// plugin assembly collection
        /// </summary>
        private Dictionary<string, PluginAssembly> pluginAssemblies;

        /// <summary>
        /// plugins collection
        /// </summary>
        private Dictionary<int, Plugin> plugins;

        /// <summary>
		/// A shared PluginManager object
        /// </summary>
        private static readonly ThreadSafeObjectProvider<PluginManager> instance = new ThreadSafeObjectProvider<PluginManager>();



        #endregion

        #region Events

        #endregion

        #region Operators

        #endregion

        #region Properties

        #endregion

        #region Methods
        /// <summary>
        /// Get a Instance of PluginManager Object.
        /// </summary>
        /// <returns>a shared PluginManager Object</returns>
        private static PluginManager GetInstance()
        {
			if (!initialize)
			{
				instance.CreateInstance(true);
				initialize = true;
			}
			return instance.Instance;
        } // GetInstance()

        /// <summary>
        /// Loads the assembly specified by AssemblyPath.
        /// </summary>
        /// <param name="assemblyPath">The full path to the assembly to be loaded.</param>
        /// <param name="friendlyName">The FriendlyName to be assigned to the loaded assembly.</param>
        /// <param name="pluginInterfaceType">The System.Type of the plugins to be loaded from the assembly.</param>
        /// <param name="shadowCopy">If set to TRUE, the assembly will be copied to temporary storage before loading, preventing the assembly from being locked by the application.</param>
        /// <param name="newAppDomain">If set to TRUE, the assembly will be copied to temporary storage before loading, preventing the assembly from being locked by the application.</param>
        /// <returns>An <see cref="PluginAssembly"/> object representing the loaded assembly.</returns>
        public static PluginAssembly LoadFromFile(
            string assemblyPath,
            string friendlyName,
            Type pluginInterfaceType,
            bool shadowCopy,
            bool newAppDomain)
        {
            PluginAssembly pluginFile;
            if (String.IsNullOrEmpty(assemblyPath))
            {
                throw new ArgumentNullException("assemblyPath");
            }

            if (pluginInterfaceType == null)
            {
                throw new ArgumentNullException("pluginInterfaceType");
            }

            if (!pluginInterfaceType.IsInterface)
            {
                throw new ArgumentException(Resources.Arg_MustBeInterface, "pluginInterfaceType");
            }

            That.Computer.FileSystem.FileExists(assemblyPath, true);

            //Load Assembly only if assembly contains any plugin
            if (!DiscoverPlugin(assemblyPath, pluginInterfaceType))
            {
                return null;
            } // if

            pluginFile = new PluginAssembly(assemblyPath, friendlyName, pluginInterfaceType, shadowCopy, newAppDomain);

            PluginManager helperInstance = GetInstance();

            helperInstance.pluginAssemblies.Add(pluginFile.FriendlyName, pluginFile);

            foreach (Plugin var in pluginFile.Plugins)
            {
                helperInstance.plugins.Add(var.ID.GetHashCode(), var);
            }

            return pluginFile;
        }

        /// <summary>
        /// Used to detect valid plugin types in assemebly, so bad assembly cannot be loaded
        /// </summary>
        /// <param name="assemblyPath">assembly path</param>
        /// <param name="pluginInterfaceType">plugin interface type</param>
        /// <returns>true if valid plugin type is found</returns>
        private static bool DiscoverPlugin(string assemblyPath, Type pluginInterfaceType)
        {
            bool ret = false;
            AppDomainLoader loader = new AppDomainLoader();
            loader.CreateDomain(assemblyPath);


            Type value;

            if (loader.Assemblies.Count != 0)
            {
                foreach (TypeLoader type in loader.Assemblies[0].Types)
                {
                    if ((type.IsPublic) && !(type.IsAbstract) && !(type.IsInterface))
                    {
                        value = type.GetInterface(pluginInterfaceType.FullName, true);

                        if (value != null)
                        {
                            if (type.IsDefined(typeof(PluginAttribute), false))
                            {
                                ret = true;
                                break;
                            }
                        } // if
                    } // if

                } // foreach 
            } // if

            loader.Dispose();
            return ret;

        } // ContainsPluginInterfaceType

        /// <summary>
        /// Loads the assembly specified by AssemblyPath.
        /// </summary>
        /// <param name="assemblyPath">The full path to the assembly to be loaded.</param>
        /// <param name="pluginInterfaceType">The System.Type of the plugins to be loaded from the assembly.</param>
        /// <returns>An <see cref="PluginAssembly"/> object representing the loaded assembly.</returns>
        public static PluginAssembly LoadFromFile(
            string assemblyPath,
            Type pluginInterfaceType)
        {
            return LoadFromFile(assemblyPath, String.Empty, pluginInterfaceType, false, false);
        }

        /// <summary>
        /// Loads the assembly specified by AssemblyPath.
        /// </summary>
        /// <param name="assemblyPath">The full path to the assembly to be loaded.</param>
        /// <param name="pluginInterfaceType">The System.Type of the plugins to be loaded from the assembly.</param>
        /// <param name="newAppDomain">If set to TRUE, the assembly will be copied to temporary storage before loading, preventing the assembly from being locked by the application.</param>
        /// <returns>An <see cref="PluginAssembly"/> object representing the loaded assembly.</returns>
        public static PluginAssembly LoadFromFile(
            string assemblyPath,
            Type pluginInterfaceType,
            bool newAppDomain)
        {
            return LoadFromFile(assemblyPath, String.Empty, pluginInterfaceType, false, newAppDomain);
        }

        /// <summary>
        /// Loads the assembly specified by AssemblyPath.
        /// </summary>
        /// <param name="assemblyPath">The full path to the assembly to be loaded.</param>
        /// <param name="pluginInterfaceType">The System.Type of the plugins to be loaded from the assembly.</param>
        /// <param name="shadowCopy">If set to TRUE, the assembly will be copied to temporary storage before loading, preventing the assembly from being locked by the application.</param>
        /// <param name="newAppDomain">If set to TRUE, the assembly will be copied to temporary storage before loading, preventing the assembly from being locked by the application.</param>
        /// <returns>An <see cref="PluginAssembly"/> object representing the loaded assembly.</returns>
        public static PluginAssembly LoadFromFile(
            string assemblyPath,
            Type pluginInterfaceType,
            bool shadowCopy,
            bool newAppDomain)
        {
            return LoadFromFile(assemblyPath, String.Empty, pluginInterfaceType, shadowCopy, newAppDomain);
        }

        /// <summary>
        /// Returns a collection containing all loaded PluginAssembly objects. 
        /// </summary>
        public static ReadOnlyCollection<PluginAssembly> LoadedAssemblies
        {
            get
            {
                PluginManager helperInstance = GetInstance();
                PluginAssembly[] pluginAssemblyArray = new PluginAssembly[helperInstance.pluginAssemblies.Values.Count];
                helperInstance.pluginAssemblies.Values.CopyTo(pluginAssemblyArray, 0);

                return new ReadOnlyCollection<PluginAssembly>(pluginAssemblyArray);
            } // get
        }

        /// <summary>
        /// Returns a collection containing all loaded plugin objects. 
        /// </summary>
        public static ReadOnlyCollection<Plugin> LoadedPlugins
        {
            get
            {
                PluginManager helperInstance = GetInstance();
                Plugin[] pluginArray = new Plugin[helperInstance.plugins.Values.Count];
                helperInstance.plugins.Values.CopyTo(pluginArray, 0);
                return new ReadOnlyCollection<Plugin>(pluginArray);
            } // get
        }

        /// <summary>
        /// Unloads all PluginAssembly objects currently loaded. 
        /// </summary>
        public static void UnloadAll()
        {
            PluginManager helperInstance = GetInstance();

            //clear plugin list
            helperInstance.plugins.Clear();

            //clear assembly List
            helperInstance.pluginAssemblies.Clear();

        } // Unload

        /// <summary>
        /// Unloads the specified plugin assembly. 
        /// </summary>
        /// <param name="assembly">The PluginAssembly object to be unloaded.</param>
        public static void Unload(PluginAssembly assembly)
        {
            if (assembly == null)
            {
                return;
            }

            PluginManager helperInstance = GetInstance();

            if (helperInstance.pluginAssemblies.ContainsKey(assembly.FriendlyName))
            {
                assembly.Dispose();
                helperInstance.pluginAssemblies.Remove(assembly.FriendlyName);
            }
        }

        /// <summary>
        /// Unloads the named assembly. 
        /// </summary>
        /// <param name="friendlyName">The FriendlyName of the PluginAssembly to be unloaded.</param>
        public static void Unload(string friendlyName)
        {
            if (String.IsNullOrEmpty(friendlyName))
            {
                return;
            }

            PluginManager helperInstance = GetInstance();

            if (helperInstance.pluginAssemblies.ContainsKey(friendlyName))
            {
                helperInstance.pluginAssemblies.Remove(friendlyName);
            }

        }

        /// <summary>
        /// Searches for and returns the loaded PluginAssembly whose FriendlyName property matches the FriendlyName argument.
        /// </summary>
        /// <param name="friendlyName">The name of the PluginAssembly object to be returned.</param>
        /// <returns>specified PluginAssembly</returns>
        public static PluginAssembly FindAssembly(string friendlyName)
        {
            PluginManager helperInstance = GetInstance();

            if (helperInstance.pluginAssemblies.ContainsKey(friendlyName))
            {
                return helperInstance.pluginAssemblies[friendlyName];
            }

            return null;

        } // FindAssembly

        /// <summary>
        /// Searches the folder specified by FolderPath for all files matching SearchPattern, and loads those asesmblies that have exposed classes supporting the PluginInterfaceType type. 
        /// </summary>
        /// <param name="folderPath">The folder to search. </param>
        /// <param name="searchPattern">The search pattern to use ( ie: "*.dll" )</param>
        /// <param name="pluginInterfaceType">The System.Type of the classes to be loaded as plugins. </param>
        /// <param name="includeSubfolders">If set to TRUE, will recursively search all subfolders below FolderPath.</param>
        public static void LoadFromFolder(string folderPath, string searchPattern, Type pluginInterfaceType, bool includeSubfolders)
        {
            LoadFromFolder(folderPath, searchPattern, pluginInterfaceType, false, false, includeSubfolders);
        } // LoadFromFolder


        /// <summary>
        /// Searches the folder specified by FolderPath for all files matching SearchPattern, and loads those asesmblies that have exposed classes supporting the PluginInterfaceType type. 
        /// </summary>
        /// <param name="folderPath">The folder to search. </param>
        /// <param name="searchPattern">The search pattern to use ( ie: "*.dll" )</param>
        /// <param name="pluginInterfaceType">The System.Type of the classes to be loaded as plugins. </param>
        /// <param name="loadInNewAppDomain">If set to TRUE, will copy the assembly to temporary storage before loading. </param>
        /// <param name="includeSubfolders">If set to TRUE, will recursively search all subfolders below FolderPath.</param>
        public static void LoadFromFolder(string folderPath, string searchPattern, Type pluginInterfaceType, bool loadInNewAppDomain, bool includeSubfolders)
        {
            LoadFromFolder(folderPath, searchPattern, pluginInterfaceType, false, loadInNewAppDomain, includeSubfolders);
        } // LoadFromFolder


        /// <summary>
        /// Searches the folder specified by FolderPath for all files matching SearchPattern, and loads those asesmblies that have exposed classes supporting the PluginInterfaceType type. 
        /// </summary>
        /// <param name="folderPath">The folder to search. </param>
        /// <param name="searchPattern">The search pattern to use ( ie: "*.dll" )</param>
        /// <param name="pluginInterfaceType">The System.Type of the classes to be loaded as plugins. </param>
        /// <param name="shadowCopy">If set to TRUE, will copy the assembly to temporary storage before loading. </param>
        /// <param name="loadInNewAppDomain">If set to TRUE, will copy the assembly to temporary storage before loading. </param>
        /// <param name="includeSubfolders">If set to TRUE, will recursively search all subfolders below FolderPath.</param>
        public static void LoadFromFolder(string folderPath, string searchPattern, Type pluginInterfaceType, bool shadowCopy, bool loadInNewAppDomain, bool includeSubfolders)
        {
            foreach (string var in That.Computer.FileSystem.GetFiles(folderPath, SearchOption.SearchTopLevelOnly, searchPattern))
            {
                LoadFromFile(var, "", pluginInterfaceType, shadowCopy, loadInNewAppDomain);
            }
        } // LoadFromFolder

        /// <summary>
        /// Returns a Collection containing all loaded plugins of the given interface type
        /// </summary>
        /// <param name="pluginInterfaceType">Type of the plugin interface.</param>
        /// <returns>
        /// Returns a Plugin Collection containing all loaded plugins of the given System.Type
        /// </returns>
        public static ReadOnlyCollection<Plugin> GetPluginsOfType(Type pluginInterfaceType)
        {
            List<Plugin> list = new List<Plugin>();

            if (pluginInterfaceType == null)
            {
                throw new ArgumentNullException("pluginInterfaceType");

            }

            if (!pluginInterfaceType.IsInterface)
            {
                throw new ArgumentException(Resources.Arg_MustBeInterface, "pluginInterfaceType");
            }

            PluginManager helperInstance = GetInstance();

            foreach (Plugin var in helperInstance.plugins.Values)
            {
                if (var.IsPluginOfType(pluginInterfaceType))
                {
                    list.Add(var);
                }
            }
            return new ReadOnlyCollection<Plugin>(list);

        } // GetPluginsOfType

        #endregion
    }
}
